# -*- coding: utf-8 -*-

# @File   : file.py
# @Author : Yuvv
# @Date   : 2017/12/3

import os
import random
from enum import Enum, IntEnum

from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.indexes import GinIndex
from django.core.files import File
from django.db import models
from django.utils import timezone
from datetime import datetime, timedelta
import hashlib


class ContentTypeMapping(Enum):
    OTHERS = 'application/octet-stream'  # 其它压缩包类的
    JSON = 'application/json'
    XML = 'application/xml'
    XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'


class ObjectContentType(IntEnum):
    ARCHIVE = 0
    JSON = 1
    XML = 2
    XLSX = 3
    DATA_IMAGE = 4
    DATA_FILE = 5

    @staticmethod
    def get_suffix(value):
        if value < ObjectContentType.DATA_IMAGE:
            return ['', '.json', '.xml', '.xlsx'][value]
        return None

    @property
    def is_others(self):
        return self.value == ObjectContentType.ARCHIVE

    @property
    def is_json(self):
        return self.value == ObjectContentType.JSON

    @property
    def is_xml(self):
        return self.value == ObjectContentType.XML

    @property
    def is_xlsx(self):
        return self.value == ObjectContentType.XLSX


class ObjectFileType(IntEnum):
    TEMPLATE = 0
    UPLOADED = 1
    DATA = 2
    DATASET = 3
    DATA_CONTENT = 4

    @property
    def is_template(self):
        return self.value == ObjectFileType.TEMPLATE

    @property
    def is_uploaded(self):
        return self.value == ObjectFileType.UPLOADED

    @property
    def is_data(self):
        return self.value == ObjectFileType.DATA

    @property
    def is_dataset(self):
        return self.value == ObjectFileType.DATASET


class ObjectFile(models.Model):
    """面向用户的数据文件，包括以下四种
    PermanentTemplateFile: misc(tid)
    PermanentUploadedFile: misc(name, author)
    TemporaryDataFile: misc(oid) 三天过期
    TemporaryDataSetFile: misc(None) 一天过期
    DataFieldImage: misc(name, author, owner_id, meta_id, path) 每天删除没有 owner_id 和 meta_id 的
    DataFieldField: misc 同上

    `misc`:   不确定部分，具体见上方
    `o_type`: 文件类型，上面几种
    `c_type`: 文件 content_type
    `add_time`: 创建时间
    `author`: 提交者名称
    `file`:   实际文件内容
    """
    misc = JSONField()
    o_type = models.IntegerField(choices=((0, 'TEMPLATE'), (1, 'UPLOADED'),
                                          (2, 'DATA'), (3, 'DATASET'), (4, 'DATA_CONTENT')))
    c_type = models.IntegerField(choices=((0, 'ARCHIVE'), (1, 'JSON'),
                                          (2, 'XML'), (3, 'XLSX'),
                                          (4, 'DATA_IMAGE'), (5, 'DATA_FILE')))
    size = models.IntegerField()
    add_time = models.DateTimeField(auto_now=True)
    file = models.FileField(upload_to='_fs/%Y/%m/%d/')

    class Meta:
        ordering = ('-add_time',)
        indexes = (models.Index(fields=['file']),
                   GinIndex(fields=['misc']))

    def to_dict(self):
        """转化为 dict 对象，文件被转化为下载链接"""
        return {'id': self.id,
                'misc': self.misc,
                'o_type': self.o_type,
                'c_type': self.c_type,
                'size': self.size,
                'add_time': timezone.localtime(self.add_time).strftime('%Y-%m-%d %H:%M:%S'),
                'file': settings.MEDIA_URL + self.file.name}

    def get_file_url(self, absolute=False, name_only=False):
        if name_only:
            return self.file.name
        if absolute:
            return os.path.join(settings.MEDIA_ROOT, self.file.name)
        return settings.MEDIA_URL + self.file.name

    def delete(self, using=None, keep_parents=False):
        super().delete(using, keep_parents)
        self.file.delete()

    @staticmethod
    def add(o_type, c_type, file, **kwargs):
        """
        添加新的数据文件
        :param o_type: 对象类型
        :param c_type: 文件类型（others/json/xml/xlsx 中的一种）
        :param file: 实际文件内容
        :param kwargs: `author`, `oid`, `tid` 可以设置
        :return: 返回添加的数据文件对象
        :rtype: ObjectFile
        """
        if isinstance(c_type, str):
            c_type = ObjectContentType[c_type]
        ppf = ObjectFile(o_type=o_type, c_type=c_type, misc=kwargs)
        f = File(file)
        ppf.size = f.size
        ext = ObjectContentType.get_suffix(c_type) or os.path.splitext(kwargs.get('name'))[1]
        new_name = ObjectFile.get_short_url()
        while os.path.exists(os.path.join(settings.MEDIA_ROOT,
                                          ObjectFile.file.field.generate_filename(None, new_name + ext))):
            new_name = ObjectFile.get_short_url()
        ppf.file.save(new_name + ext, f)
        ppf.save()
        return ppf

    @staticmethod
    def get_short_url(n=12):
        """获取随机短链"""
        return ''.join(
            [random.choice('abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(n)])


class FileContentType(IntEnum):
    OTHERS = 0
    JSON = 1
    XML = 2
    XLSX = 3
    IMAGE = 4
    ARCHIVE = 5
    MODEL = 6

    @property
    def default_extension(self):
        if self == FileContentType.JSON:
            return '.json'
        elif self == FileContentType.XML:
            return '.xml'
        elif self == FileContentType.XLSX:
            return '.xlsx'
        elif self == FileContentType.IMAGE:
            return '.jpg'
        elif self == FileContentType.MODEL:
            return '.model'
        else:
            return ''

    @property
    def is_xlsx(self):
        return self == FileContentType.XLSX

    @property
    def is_json(self):
        return self == FileContentType.JSON

    @property
    def is_xml(self):
        return self == FileContentType.XML


def _expires(days):
    return datetime.now() + timedelta(days=days)


class AbstractFile(models.Model):
    class Meta:
        abstract = True

    created_at = models.DateTimeField(auto_now_add=True)
    filename = models.CharField(max_length=255)
    file = models.FileField(upload_to='_fs/%Y/%m/%d/')
    content_type = models.IntegerField()
    hash_value = models.CharField(max_length=256)

    def get_url(self, absolute=False):
        if absolute:
            return settings.SITE_ADDR + settings.SITE_BASE_URL + settings.MEDIA_URL + self.file.name
        return settings.MEDIA_URL + self.file.name

    def get_system_path(self):
        return os.path.join(settings.MEDIA_ROOT, self.file.name)

    @classmethod
    def add(cls, fp, content_type, **kwargs):
        fp.seek(0)
        hash_f = hashlib.sha1()
        while True:
            data = fp.read(524288)
            if not data:
                break
            hash_f.update(data)
        hash_value = hash_f.hexdigest()
        try:
            return cls.objects.get(hash_value=hash_value)
        except cls.DoesNotExist:
            pass
        instance = cls(content_type=content_type, **kwargs)
        instance.hash_value = hash_value
        old_filename = getattr(fp, 'name', '')
        random_part = ''.join(
            [random.choice('abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(0, 6)])
        if old_filename:
            instance.filename = old_filename
            new_filename = f'{random_part}_{old_filename}'
        else:
            ext = content_type.default_extension
            new_filename = f'{datetime.now().strftime("%H%m%s")}_{random_part}{ext}'
            instance.filename = new_filename
        instance.file.save(new_filename, fp)
        return instance


class UploadedSourceFile(AbstractFile):
    uploaded_by = models.CharField(max_length=255, null=True)
    file = models.FileField(upload_to='_fs/source/%Y/%m/%d/')


class DataContentFile(AbstractFile):
    uploaded_by = models.CharField(max_length=255, null=True)
    file = models.FileField(upload_to='_fs/data_files/%Y/%m/%d/')
    meta_id = models.ForeignKey('storage.DataMeta', on_delete=models.CASCADE, null=True)


class TemporaryUploadedFile(AbstractFile):
    file = models.FileField(upload_to='_fs/temporary_uploaded/%Y/%m/%d/')


class TemporaryExportedFile(AbstractFile):
    file = models.FileField(upload_to='_fs/exported/%Y/%m/%d/')
    expires = models.DateTimeField(default=_expires(5))


class TemporaryDataTemplate(AbstractFile):
    template = models.OneToOneField('storage.Template', on_delete=models.CASCADE)
    file = models.FileField(upload_to='_fs/data_template/%Y/%m/%d/')
    expires = models.DateTimeField(default=_expires(15))
